<?php

/**
 * PHPTAL templating engine
 *
 * PHP Version 5
 *
 * @category HTML
 * @package  PHPTAL
 * @author   Laurent Bedubourg <lbedubourg@motion-twin.com>
 * @author   Kornel Lesiński <kornel@aardvarkmedia.co.uk>
 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
 * @version  SVN: $Id$
 * @link     http://phptal.org/
 */

/**
 * Document Tag representation.
 *
 * @package PHPTAL
 * @subpackage Dom
 */
class PHPTAL_Dom_Element extends PHPTAL_Dom_Node {

  protected $qualifiedName, $namespace_uri;
  private $attribute_nodes = array();
  protected $replaceAttributes = array();
  protected $contentAttributes = array();
  protected $surroundAttributes = array();
  public $headFootDisabled = false;
  public $headPrintCondition = false;
  public $footPrintCondition = false;
  public $hidden = false;
  // W3C DOM interface
  public $childNodes = array();
  public $parentNode;

  /**
   * @param string $qname           qualified name of the element, e.g. "tal:block"
   * @param string $namespace_uri   namespace of this element
   * @param array  $attribute_nodes array of PHPTAL_Dom_Attr elements
   * @param object $xmlns           object that represents namespaces/prefixes known in element's context
   */
  public function __construct($qname, $namespace_uri, array $attribute_nodes, PHPTAL_Dom_XmlnsState $xmlns) {
    $this->qualifiedName = $qname;
    $this->attribute_nodes = $attribute_nodes;
    $this->namespace_uri = $namespace_uri;
    $this->xmlns = $xmlns;

    // implements inheritance of element's namespace to tal attributes (<metal: use-macro>)
    foreach ($attribute_nodes as $index => $attr) {
      // it'll work only when qname == localname, which is good
      if ($this->xmlns->isValidAttributeNS($namespace_uri, $attr->getQualifiedName())) {
        $this->attribute_nodes[$index] = new PHPTAL_Dom_Attr($attr->getQualifiedName(), $namespace_uri, $attr->getValueEscaped(), $attr->getEncoding());
      }
    }

    if ($this->xmlns->isHandledNamespace($this->namespace_uri)) {
      $this->headFootDisabled = true;
    }

    $talAttributes = $this->separateAttributes();
    $this->orderTalAttributes($talAttributes);
  }

  /**
   * returns object that represents namespaces known in element's context
   */
  public function getXmlnsState() {
    return $this->xmlns;
  }

  /**
   * Replace <script> foo &gt; bar </script>
   * with <script>/*<![CDATA[* / foo > bar /*]]>* /</script>
   * This avoids gotcha in text/html.
   *
   * Note that PHPTAL_Dom_CDATASection::generate() does reverse operation, if needed!
   *
   * @return void
   */
  private function replaceTextWithCDATA() {
    $isCDATAelement = PHPTAL_Dom_Defs::getInstance()->isCDATAElementInHTML($this->getNamespaceURI(), $this->getLocalName());

    if (!$isCDATAelement) {
      return;
    }

    $valueEscaped = ''; // sometimes parser generates split text nodes. "normalisation" is needed.
    $value = '';
    foreach ($this->childNodes as $node) {
      // leave it alone if there is CDATA, comment, or anything else.
      if (!$node instanceof PHPTAL_Dom_Text)
        return;

      $value .= $node->getValue();
      $valueEscaped .= $node->getValueEscaped();

      $encoding = $node->getEncoding(); // encoding of all nodes is the same
    }

    // only add cdata if there are entities
    // and there's no ${structure} (because it may rely on cdata syntax)
    if (false === strpos($valueEscaped, '&') || preg_match('/<\?|\${structure/', $value)) {
      return;
    }

    $this->childNodes = array();

    // appendChild sets parent
    $this->appendChild(new PHPTAL_Dom_Text('/*', $encoding));
    $this->appendChild(new PHPTAL_Dom_CDATASection('*/' . $value . '/*', $encoding));
    $this->appendChild(new PHPTAL_Dom_Text('*/', $encoding));
  }

  public function appendChild(PHPTAL_Dom_Node $child) {
    if ($child->parentNode)
      $child->parentNode->removeChild($child);
    $child->parentNode = $this;
    $this->childNodes[] = $child;
  }

  public function removeChild(PHPTAL_Dom_Node $child) {
    foreach ($this->childNodes as $k => $node) {
      if ($child === $node) {
        $child->parentNode = null;
        array_splice($this->childNodes, $k, 1);
        return;
      }
    }
    throw new PHPTAL_Exception("Given node is not child of " . $this->getQualifiedName());
  }

  public function replaceChild(PHPTAL_Dom_Node $newElement, PHPTAL_Dom_Node $oldElement) {
    foreach ($this->childNodes as $k => $node) {
      if ($node === $oldElement) {
        $oldElement->parentNode = NULL;

        if ($newElement->parentNode)
          $newElement->parentNode->removeChild($child);
        $newElement->parentNode = $this;

        $this->childNodes[$k] = $newElement;
        return;
      }
    }
    throw new PHPTAL_Exception("Given node is not child of " . $this->getQualifiedName());
  }

  public function generateCode(PHPTAL_Php_CodeWriter $codewriter) {
    try {
      /// self-modifications

      if ($codewriter->getOutputMode() === PHPTAL::XHTML) {
        $this->replaceTextWithCDATA();
      }

      /// code generation

      if ($this->getSourceLine()) {
        $codewriter->doComment('tag "' . $this->qualifiedName . '" from line ' . $this->getSourceLine());
      }

      $this->generateSurroundHead($codewriter);

      if (count($this->replaceAttributes)) {
        foreach ($this->replaceAttributes as $att) {
          $att->before($codewriter);
          $att->after($codewriter);
        }
      } elseif (!$this->hidden) {
        // a surround tag may decide to hide us (tal:define for example)
        $this->generateHead($codewriter);
        $this->generateContent($codewriter);
        $this->generateFoot($codewriter);
      }

      $this->generateSurroundFoot($codewriter);
    } catch (PHPTAL_TemplateException $e) {
      $e->hintSrcPosition($this->getSourceFile(), $this->getSourceLine());
      throw $e;
    }
  }

  /**
   * Array with PHPTAL_Dom_Attr objects
   *
   * @return array
   */
  public function getAttributeNodes() {
    return $this->attribute_nodes;
  }

  /**
   * Replace all attributes
   *
   * @param array $nodes array of PHPTAL_Dom_Attr objects
   */
  public function setAttributeNodes(array $nodes) {
    $this->attribute_nodes = $nodes;
  }

  /** Returns true if the element contains specified PHPTAL attribute. */
  public function hasAttribute($qname) {
    foreach ($this->attribute_nodes as $attr)
      if ($attr->getQualifiedName() == $qname)
        return true;
    return false;
  }

  public function hasAttributeNS($ns_uri, $localname) {
    return null !== $this->getAttributeNodeNS($ns_uri, $localname);
  }

  public function getAttributeNodeNS($ns_uri, $localname) {
    foreach ($this->attribute_nodes as $attr) {
      if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname)
        return $attr;
    }
    return null;
  }

  public function removeAttributeNS($ns_uri, $localname) {
    foreach ($this->attribute_nodes as $k => $attr) {
      if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname) {
        unset($this->attribute_nodes[$k]);
        return;
      }
    }
  }

  public function getAttributeNode($qname) {
    foreach ($this->attribute_nodes as $attr)
      if ($attr->getQualifiedName() === $qname)
        return $attr;
    return null;
  }

  /**
   * If possible, use getAttributeNodeNS and setAttributeNS.
   *
   * NB: This method doesn't handle namespaces properly.
   */
  public function getOrCreateAttributeNode($qname) {
    if ($attr = $this->getAttributeNode($qname))
      return $attr;

    $attr = new PHPTAL_Dom_Attr($qname, "", null, 'UTF-8'); // FIXME: should find namespace and encoding
    $this->attribute_nodes[] = $attr;
    return $attr;
  }

  /** Returns textual (unescaped) value of specified element attribute. */
  public function getAttributeNS($namespace_uri, $localname) {
    if ($n = $this->getAttributeNodeNS($namespace_uri, $localname)) {
      return $n->getValue();
    }
    return '';
  }

  /**
   * Set attribute value. Creates new attribute if it doesn't exist yet.
   *
   * @param string $namespace_uri full namespace URI. "" for default namespace
   * @param string $qname prefixed qualified name (e.g. "atom:feed") or local name (e.g. "p")
   * @param string $value unescaped value
   *
   * @return void
   */
  public function setAttributeNS($namespace_uri, $qname, $value) {
    $localname = preg_replace('/^[^:]*:/', '', $qname);
    if (!($n = $this->getAttributeNodeNS($namespace_uri, $localname))) {
      $this->attribute_nodes[] = $n = new PHPTAL_Dom_Attr($qname, $namespace_uri, null, 'UTF-8'); // FIXME: find encoding
    }
    $n->setValue($value);
  }

  /**
   * Returns true if this element or one of its PHPTAL attributes has some
   * content to print (an empty text node child does not count).
   *
   * @return bool
   */
  public function hasRealContent() {
    if (count($this->contentAttributes) > 0)
      return true;

    foreach ($this->childNodes as $node) {
      if (!$node instanceof PHPTAL_Dom_Text || $node->getValueEscaped() !== '')
        return true;
    }
    return false;
  }

  public function hasRealAttributes() {
    if ($this->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'attributes'))
      return true;
    foreach ($this->attribute_nodes as $attr) {
      if ($attr->getReplacedState() !== PHPTAL_Dom_Attr::HIDDEN)
        return true;
    }
    return false;
  }

  // ~~~~~ Generation methods may be called by some PHPTAL attributes ~~~~~

  public function generateSurroundHead(PHPTAL_Php_CodeWriter $codewriter) {
    foreach ($this->surroundAttributes as $att) {
      $att->before($codewriter);
    }
  }

  public function generateHead(PHPTAL_Php_CodeWriter $codewriter) {
    if ($this->headFootDisabled)
      return;
    if ($this->headPrintCondition) {
      $codewriter->doIf($this->headPrintCondition);
    }

    $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);

    if ($html5mode) {
      $codewriter->pushHTML('<' . $this->getLocalName());
    } else {
      $codewriter->pushHTML('<' . $this->qualifiedName);
    }

    $this->generateAttributes($codewriter);

    if (!$html5mode && $this->isEmptyNode($codewriter->getOutputMode())) {
      $codewriter->pushHTML('/>');
    } else {
      $codewriter->pushHTML('>');
    }

    if ($this->headPrintCondition) {
      $codewriter->doEnd('if');
    }
  }

  public function generateContent(PHPTAL_Php_CodeWriter $codewriter = null, $realContent = false) {
    if (!$this->isEmptyNode($codewriter->getOutputMode())) {
      if ($realContent || !count($this->contentAttributes)) {
        foreach ($this->childNodes as $child) {
          $child->generateCode($codewriter);
        }
      } else
        foreach ($this->contentAttributes as $att) {
          $att->before($codewriter);
          $att->after($codewriter);
        }
    }
  }

  public function generateFoot(PHPTAL_Php_CodeWriter $codewriter) {
    if ($this->headFootDisabled)
      return;
    if ($this->isEmptyNode($codewriter->getOutputMode()))
      return;

    if ($this->footPrintCondition) {
      $codewriter->doIf($this->footPrintCondition);
    }

    if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
      $codewriter->pushHTML('</' . $this->getLocalName() . '>');
    } else {
      $codewriter->pushHTML('</' . $this->getQualifiedName() . '>');
    }

    if ($this->footPrintCondition) {
      $codewriter->doEnd('if');
    }
  }

  public function generateSurroundFoot(PHPTAL_Php_CodeWriter $codewriter) {
    for ($i = (count($this->surroundAttributes) - 1); $i >= 0; $i--) {
      $this->surroundAttributes[$i]->after($codewriter);
    }
  }

  // ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  private function generateAttributes(PHPTAL_Php_CodeWriter $codewriter) {
    $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);

    foreach ($this->getAttributeNodes() as $attr) {

      // xmlns:foo is not allowed in text/html
      if ($html5mode && $attr->isNamespaceDeclaration()) {
        continue;
      }

      switch ($attr->getReplacedState()) {
        case PHPTAL_Dom_Attr::NOT_REPLACED:
          $codewriter->pushHTML(' ' . $attr->getQualifiedName());
          if ($codewriter->getOutputMode() !== PHPTAL::HTML5 || !PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
            $html = $codewriter->interpolateHTML($attr->getValueEscaped());
            $codewriter->pushHTML('=' . $codewriter->quoteAttributeValue($html));
          }
          break;

        case PHPTAL_Dom_Attr::HIDDEN:
          break;

        case PHPTAL_Dom_Attr::FULLY_REPLACED:
          $codewriter->pushHTML($attr->getValueEscaped());
          break;

        case PHPTAL_Dom_Attr::VALUE_REPLACED:
          $codewriter->pushHTML(' ' . $attr->getQualifiedName() . '="');
          $codewriter->pushHTML($attr->getValueEscaped());
          $codewriter->pushHTML('"');
          break;
      }
    }
  }

  private function isEmptyNode($mode) {
    return (($mode === PHPTAL::XHTML || $mode === PHPTAL::HTML5) && PHPTAL_Dom_Defs::getInstance()->isEmptyTagNS($this->getNamespaceURI(), $this->getLocalName())) ||
            ( $mode === PHPTAL::XML && !$this->hasContent());
  }

  private function hasContent() {
    return count($this->childNodes) > 0 || count($this->contentAttributes) > 0;
  }

  private function separateAttributes() {
    $talAttributes = array();
    foreach ($this->attribute_nodes as $index => $attr) {
      // remove handled xml namespaces
      if (PHPTAL_Dom_Defs::getInstance()->isHandledXmlNs($attr->getQualifiedName(), $attr->getValueEscaped())) {
        unset($this->attribute_nodes[$index]);
      } else if ($this->xmlns->isHandledNamespace($attr->getNamespaceURI())) {
        $talAttributes[$attr->getQualifiedName()] = $attr;
        $attr->hide();
      } else if (PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
        $attr->setValue($attr->getLocalName());
      }
    }
    return $talAttributes;
  }

  private function orderTalAttributes(array $talAttributes) {
    $temp = array();
    foreach ($talAttributes as $key => $domattr) {
      $nsattr = PHPTAL_Dom_Defs::getInstance()->getNamespaceAttribute($domattr->getNamespaceURI(), $domattr->getLocalName());
      if (array_key_exists($nsattr->getPriority(), $temp)) {
        throw new PHPTAL_TemplateException(sprintf("Attribute conflict in < %s > '%s' cannot appear with '%s'", $this->qualifiedName, $key, $temp[$nsattr->getPriority()][0]->getNamespace()->getPrefix() . ':' . $temp[$nsattr->getPriority()][0]->getLocalName()
        ), $this->getSourceFile(), $this->getSourceLine());
      }
      $temp[$nsattr->getPriority()] = array($nsattr, $domattr);
    }
    ksort($temp);

    $this->talHandlers = array();
    foreach ($temp as $prio => $dat) {
      list($nsattr, $domattr) = $dat;
      $handler = $nsattr->createAttributeHandler($this, $domattr->getValue());
      $this->talHandlers[$prio] = $handler;

      if ($nsattr instanceof PHPTAL_NamespaceAttributeSurround)
        $this->surroundAttributes[] = $handler;
      else if ($nsattr instanceof PHPTAL_NamespaceAttributeReplace)
        $this->replaceAttributes[] = $handler;
      else if ($nsattr instanceof PHPTAL_NamespaceAttributeContent)
        $this->contentAttributes[] = $handler;
      else
        throw new PHPTAL_ParserException("Unknown namespace attribute class " . get_class($nsattr), $this->getSourceFile(), $this->getSourceLine());
    }
  }

  function getQualifiedName() {
    return $this->qualifiedName;
  }

  function getNamespaceURI() {
    return $this->namespace_uri;
  }

  function getLocalName() {
    $n = explode(':', $this->qualifiedName, 2);
    return end($n);
  }

  function __toString() {
    return '<{' . $this->getNamespaceURI() . '}:' . $this->getLocalName() . '>';
  }

  function setValueEscaped($e) {
    throw new PHPTAL_Exception("Not supported");
  }

}
